TD1 - Template based class ************************** Nous vous proposons de développer une classe template SquareMatrix paramétrée par * Le type des valeurs qu'elle contient * Sa dimension Vous allez implémenter les fonctionnalités dans l'ordre demandé en les validant à chaque fois par des tests. Constructeurs et affichage ========================== Complétez le code suivant pour que la classe *SquareMatrix* soit fonctionnelle : * Un constructeur par défaut sans initialisation des valeurs. * Un constructeur initialisant la matrice ligne par ligne à partir de valeurs entières passées en paramètre. * Une fonction d'affichage *print()*. .. code-block:: cpp template ... class SquareMatrix { private: // données internes ... public: // Constructeur sans argument SquareMatrix() {} // Constructeur recevant une série de valeurs SquareMatrix(const array<...> & Data) {...} // affichage void print() {...} } L'affichage produit par la fonction *print* est optimisée pour les nombres entiers entre 0 et 99. Le format d'affichage à respecter est le suivant : * Deux colonnes par valeur. * Une colonne d'espacement entre les valeurs. Vous devez mettre en place un code capable d'exécuter la fonction *test1()* fournie ci-dessous. Vous devez obtenir un affichage identique. .. code-block:: cpp const int N=3; using SM3 = SquareMatrix; void test1() { // Initialisation avec valeurs SM3 M({ 11,2,3, 4,55,6, 7,8,99 }); M.print(); cout << "Fin du test 1------------" << endl; } >> 11 2 3 >> 4 55 6 >> 7 8 99 Il faut fixer un type pour le paramètre du constructeur. L'écriture *{1,2,3,4,5,6,7,8,9}* est compatible avec l’initialisation d'un *array* (ou d'un vector). Comme nous voulons utiliser cette écriture compacte et pratique, nous choisissons donc le type *array* pour notre paramètre car il correspond à une liste de taille fixe ce qui est notre contexte. Dans l'écriture : .. code-block:: cpp SquareMatrix(const array<...> & T) {...} Nous avons utilisé une *const* référence, pourquoi ? * Un utilisateur peut décider de passer un objet *array* à cette fonction. Il peut le faire puisqu'elle accepte des *array* ! Dans cette situation, la référence évitera une recopie. * Le qualificatif *const* permet d'indiquer au développeur que l'argument passé n'est pas modifié. .. note:: Comment peut-on avoir une référence sur un objet alors qu'aucun objet *array* n'a été créé ? En effet, il y a une création implicite d'un objet anonyme suite à l'appel du constructeur. Cet objet sera associé à la référence. Opérateurs d'accès ================== L'opérateur d'indexation [] n'accepte qu'un seul argument. Il n'est donc pas possible en C++ de mettre en place une syntaxe de la forme *[i,j]*. Il est possible de chaîner deux opérateurs d'indexation en écrivant *[i][j]* mais cela suppose que l'on créé une classe intermédiaire pour retourner une ligne de la matrice ce qui complexifie un peu l'exercice. Pour l'instant, nous choisissons l'option la plus simple en surchargeant l'opérateur () et lui transmettant les deux indices *i* et *j* : *(i,j)*. Nous vous demandons de respecter la convention suivante : .. image:: matrice.jpg :align: center :scale: 25% Version **NON-CONST** : .. code-block:: cpp // Accès aux éléments (lecture/écriture) T& operator()(int i, int j) { ... } Cet opérateur retourne une référence alors que nous avons conseillé plusieurs fois précédemment de retourner des objets temporaires ! En effet, cette affirmation avait été faite dans le cas du retour d'un objet créé dans la corps de l'opérateur ET on ne peut retourner une référence vers un objet local qui s'apprête à être détruit. Dans notre cas, on peut retourner une référence car les données de la matrice sont pérennes. Cette référence permet d'éviter une recopie et elle sert de l-value dans la syntaxe suivante : .. code-block:: cpp M(i,j) = 5; Version **CONST** : .. code-block:: cpp const T& operator()(int i, int j) const { ... } On pourrait se servir uniquement de la version *non-const* mais à un moment, C++ oblige, le compilateur va finir par se plaindre ! En effet, une fonction peut prendre en argument une const référence vers une matrice, elle a le droit et c'est judicieux si par exemple elle ne fait qu'afficher ses valeurs. Cependant, la const référence ne permet d'utiliser que les membres *const* de la classe matrice, ceux qui sont déclarés comme ne modifiant pas les données internes. Par conséquent, dans ce cas, il faut pouvoir disposer d'un accesseur *const* sous peine de ne pas pouvoir faire grand chose :) Vous devez mettre en place une classe capable d'exécuter le code de test fourni ci-dessous : .. code-block:: cpp const int N=3; using SM3 = SquareMatrix; void fnt2(const SM3 & M) { cout << M(0, 0); } void test2() { // Exemple d'utilisation SM3 M; for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) M(i, j) = i * i + j; for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) if ( M(i, j) != i * i + j ) cout << "ERREUR"; fnt2(M); cout << "Test 2 terminé" << endl; } Initialiseurs supplémentaires ============================= Ajoutez à la classe matrice les fonctions suivantes : * Une fonction membre *rand()* permettant d'initialiser la matrice actuelle avec des entiers choisis aléatoirement entre 0 et 99. * Une fonction membre *diag()* initialisant la matrice pour qu'elle corresponde à une matrice diagonale. * Une fonction membre *fill(...)* qui initialise les valeurs de la matrice à partir de la valeur fournie en argument. Effectuez des affichages pour contrôler votre travail. Transposition ============= Ajoutez : * Une fonction membre *transpose()* qui transpose les valeurs actuelles de la matrice (inplace). * Une fonction externe *... t(...)* qui retourne une nouvelle matrice correspondant à la transposée de la matrice passée en paramètre. Écrivez un test pour valider votre implémentation. Affichage ========= A partir de maintenant, toutes les fonctions ajoutées dans le code sont des fonctions externes à la classe (pas de fonction membre). Surchargez l'opérateur << pour afficher le contenu de la matrice dans le même format que précédemment. .. code-block:: cpp ostream& operator << (ostream& os, ... ) { ... } Lorsque l'on écrit : *cout << M*, l'argument de gauche correspond à un objet de type *ostream*. Il ne faut pas le recopier, ce qui explique le passage par référence. Le paramètre de droite correspond à la matrice transmise lors de l'appel. Choisissez judicieusement le type associé. N'oubliez pas de chaîner votre opérateur en retournant l'objet *cout* afin de permettre l'écriture suivante : *cout << M1 << M2 << endl;* Écrivez un test pour valider votre implémentation. .. note:: Vous pouvez appeler la fonction *print()* dans cet opérateur plutôt que d'effectuer un copié-collé. Opérateurs ========== Mettez en place les opérateurs suivant : * Addition et soustraction de deux matrices * Multiplication entre deux matrices * Multiplication d'une matrice par un scalaire (gauche et droite) Comme nous créons des opérateurs externes (non membre de la classe), ils ne peuvent accéder aux données privées et doivent donc utiliser les accesseurs publics. Cependant, si vous voulez accéder aux données privées depuis une classe, un opérateur ou une fonction externe, alors il faut déclarer cette entité comme amie. Pour cela, on écrit la déclaration de l'entité dans la classe *SquareMatrix* en ajoutant le qualificatif **friend**, comme ceci : .. code-block:: cpp template ... class SquareMatrix { ... template ... friend ... operator+(...,...); }; Test final ========== Grâce à votre code, effectuez le calcul suivant : .. math:: A = A \cdot A^t + 2 \cdot A - A \cdot A Utilisez la matrice *A= [ [1,2,3], [2,3,4], [3,4,5]]* pour tester votre résultat. Vous devez obtenir : .. code-block:: cpp >> 2 4 6 >> 4 6 8 >> 6 8 10